import AR_TOOLKIT_NFT_WASM from "../libs/artoolkitNFT_ES6_wasm.js";
import { camera_para } from "./camera_para.js"
export class ArToolkitNFT {
    constructor(width, height, cameraParam) {
        this.id = -1;

        this.width = width;
        this.height = height;

        this.cameraParam = cameraParam || camera_para;
        this.cameraId = -1;
        this.cameraLoaded = false;
        this.artoolkitNFT = null;
        this.listeners = {};

        this.nftMarkers = {};

        this.transform_mat = new Float64Array(16);
        this.transformGL_RH = new Float64Array(16);
        this.marker_transform_mat = null;

        this.videoWidth = width;
        this.videoHeight = height;
        this.videoSize = this.videoWidth * this.videoHeight;

        this.framepointer = null;
        this.framesize = null;
        this.dataHeap = null;
        this.videoLuma = null;
        this.camera_mat = null;
        this.videoLumaPointer = null;

        this.nftMarkerFound = false;
        this.nftMarkerFoundTime = 0;
        this.nftMarkerCount = 0;

        this._bwpointer = null;
        this.defaultMarkerWidth = 1;

        this.instance = null
        this.markerNFTCount = 0;
        this.cameraCount = 0;
        this.NFT_MARKER = 0
    }
    static async initWithDimensions(width, height, cameraParam) {
        const arControllerNFT = new ArToolkitNFT(width, height, cameraParam);
        return await arControllerNFT._initialize();
    }
    async _initialize() {
        try {
            await this.init();
            console.log("[ARControllerNFT]", "ARToolkitNFT initialized");

            this.cameraId = await this.loadCamera(this.cameraParam);
            console.log(
                "[ARControllerNFT]",
                "Camera params loaded with ID",
                this.cameraId
            );
            this.id = this.setup(this.width, this.height, this.cameraId);

            this._initNFT();

            const params = this.frameMalloc;
            this.framepointer = params.framepointer;
            this.framesize = params.framesize;
            this.videoLumaPointer = params.videoLumaPointer;

            this.dataHeap = new Uint8Array(
                this.instance.HEAPU8.buffer,
                this.framepointer,
                this.framesize
            );
            this.videoLuma = new Uint8Array(
                this.instance.HEAPU8.buffer,
                this.videoLumaPointer,
                this.framesize / 4
            );

            this.camera_mat = new Float64Array(
                this.instance.HEAPU8.buffer,
                params.camera,
                16
            );
            this.marker_transform_mat = new Float64Array(
                this.instance.HEAPU8.buffer,
                params.transform,
                12
            );

            this.setProjectionNearPlane(this.id, 0.1);
            this.setProjectionFarPlane(this.id, 1000);

            setTimeout(() => {
                this.dispatchEvent({
                    name: "load",
                    target: this,
                });
            }, 1);
            return this;
        } catch (err) {
            console.log(err)
        }

    }
    async init() {
        this.instance = await AR_TOOLKIT_NFT_WASM();
        this._decorate();
        let scope = typeof window !== "undefined" ? window : global;
        scope.artoolkitNFT = this;
        return this;
    }
    _initNFT() {
        this.setupAR2(this.id);
    }
    _decorate() {
        [
            "setup",
            "teardown",

            "setupAR2",

            "setLogLevel",
            "getLogLevel",

            "setDebugMode",
            "getDebugMode",

            "getProcessingImage",

            "detectMarker",
            "detectNFTMarker",
            "getNFTMarker",
            "getNFTData",

            "frameMalloc",
            "NFTMarkerInfo",

            "setProjectionNearPlane",
            "getProjectionNearPlane",

            "setProjectionFarPlane",
            "getProjectionFarPlane",

            "setThresholdMode",
            "getThresholdMode",

            "setThreshold",
            "getThreshold",

            "setImageProcMode",
            "getImageProcMode",

            "StringList",
        ].forEach((method) => {
            this[method] = this.instance[method];
        });

        // expose constants
        for (const co in this.instance) {
            if (co.match(/^AR/)) {
                this[co] = this.instance[co];
            }
        }
    }
    async loadCamera(urlOrData) {
        const target = "/camera_param_" + this.cameraCount++;
        let data;
        if (urlOrData instanceof Uint8Array) {
            data = urlOrData;
        } else {
            try {
                data = await this.fetchRemoteData(urlOrData);
            } catch (error) {
                throw error;
            }
        }

        this._storeDataFile(data, target);
        return this.instance._loadCamera(target);
    }
    _storeDataFile(data, target) {
        this.instance.FS.writeFile(target, data, {
            encoding: "binary",
        });
    }
    dispatchEvent(event) {
        let listeners = this.listeners[event.name];
        if (listeners) {
            for (let i = 0; i < listeners.length; i++) {
                listeners[i].call(this, event);
            }
        }
    }
    getCameraMatrix() {
        return this.camera_mat;
    }
    async loadNFTMarker(urlOrData, onSuccess, onError) {
        let nft = await this.addNFTMarkers(
            this.id,
            [urlOrData],
            (ids) => {
                this.nftMarkerCount += ids.length;
                onSuccess(ids);
            },
            onError
        );
        return nft;
    }
    addNFTMarkers(arId, urls, callback, onError2) {
        let prefixes = [];
        let pending = urls.length * 3;
        let onSuccess = (filename) => {
            pending -= 1;
            if (pending === 0) {
                const vec = new this.instance.StringList();
                const markerIds = [];
                for (let i = 0; i < prefixes.length; i++) {
                    vec.push_back(prefixes[i]);
                }
                let ret = this.instance._addNFTMarkers(arId, vec);
                for (let i = 0; i < ret.size(); i++) {
                    markerIds.push(ret.get(i));
                }

                console.log("add nft marker ids: ", markerIds);
                if (callback) callback(markerIds);
            }
        };
        let onError = (filename, errorNumber) => {
            console.log("failed to load: ", filename);
            onError2(errorNumber);
        };

        let Ids = [];

        urls.forEach((element, index) => {
            let prefix = "/markerNFT_" + this.markerNFTCount;
            prefixes.push(prefix);
            let filename = {
                0: prefix + ".fset",
                1: prefix + ".iset",
                2: prefix + ".fset3",
            }
            if (Array.isArray(element)) {
                element.forEach((url, index) => {
                    this.ajax(
                        url,
                        filename[index],
                        onSuccess.bind(filename[index]),
                        onError.bind(filename[index])
                    );
                });
            } else {

                this.ajax(
                    element + ".fset",
                    filename["0"],
                    onSuccess.bind(filename["0"]),
                    onError.bind(filename["0"])
                );
                this.ajax(
                    element + ".iset",
                    filename["1"],
                    onSuccess.bind(filename["1"]),
                    onError.bind(filename["1"])
                );
                this.ajax(
                    element + ".fset3",
                    filename["2"],
                    onSuccess.bind(filename["2"]),
                    onError.bind(filename["2"])
                );

                this.markerNFTCount += 1;
            }

            Ids.push(index);
        });

        return Ids;
    }
    trackNFTMarkerId(id, markerWidth) {
        let obj = this.nftMarkers[id];
        if (!obj) {
            this.nftMarkers[id] = obj = {
                inPrevious: false,
                inCurrent: false,
                matrix: new Float64Array(12),
                matrixGL_RH: new Float64Array(12),
                markerWidth: markerWidth || this.defaultMarkerWidth,
            };
        }
        if (markerWidth) {
            obj.markerWidth = markerWidth;
        }
        return obj;
    }
    ajax(url, target, callback, errorCallback) {
        let oReq = new XMLHttpRequest();
        oReq.open("GET", url, true);
        oReq.responseType = "arraybuffer"; // blob arraybuffer
        const writeByteArrayToFS = (
            target,
            byteArray,
            callback
        ) => {
            this.instance.FS.writeFile(target, byteArray, { encoding: "binary" });
            callback(byteArray);
        };

        oReq.onload = function () {
            if (this.status == 200) {
                let arrayBuffer = oReq.response;
                let byteArray = new Uint8Array(arrayBuffer);
                writeByteArrayToFS(target, byteArray, callback);
            } else {
                errorCallback(this.status);
            }
        };

        oReq.send();
    }
    process(image) {
        let result = this._copyImageToHeap(image);
        if (result != 0) {
            console.error("[ARControllerNFT]", "detectMarker error:", result);
        }

        let k, o;

        // get NFT markers
        for (k in this.nftMarkers) {
            o = this.nftMarkers[k];
            o.inPrevious = o.inCurrent;
            o.inCurrent = false;
        }

        // detect NFT markers
        let nftMarkerCount = this.nftMarkerCount;
        this.detectNFTMarker(this.id);

        // in ms
        const MARKER_LOST_TIME = 200;

        for (let i = 0; i < nftMarkerCount; i++) {
            let nftMarkerInfo = this.getMarker(i);

            let markerType = this.NFT_MARKER;

            if (nftMarkerInfo.found) {
                this.nftMarkerFound = i;
                this.nftMarkerFoundTime = Date.now();

                let visible = this.trackNFTMarkerId(i);
                visible.matrix.set(nftMarkerInfo.pose);
                visible.inCurrent = true;
                this.transMatToGLMat(visible.matrix, this.transform_mat);
                this.transformGL_RH = this.arglCameraViewRHf(this.transform_mat);
                this.dispatchEvent({
                    name: "getNFTMarker",
                    target: this,
                    data: {
                        index: i,
                        type: markerType,
                        marker: nftMarkerInfo,
                        matrix: this.transform_mat,
                        matrixGL_RH: this.transformGL_RH,
                    },
                });
            } else if (this.nftMarkerFound === i) {
                if (Date.now() - this.nftMarkerFoundTime > MARKER_LOST_TIME) {
                    this.nftMarkerFound = false;
                    this.dispatchEvent({
                        name: "lostNFTMarker",
                        target: this,
                        data: {
                            index: i,
                            type: markerType,
                            marker: nftMarkerInfo,
                            matrix: this.transform_mat,
                            matrixGL_RH: this.transformGL_RH,
                        },
                    });
                }
            }
        }
    }
    arglCameraViewRHf(glMatrix, glRhMatrix, scale) {
        let m_modelview;
        if (glRhMatrix == undefined) {
            m_modelview = new Float64Array(16);
        } else {
            m_modelview = glRhMatrix;
        }

        // x
        m_modelview[0] = glMatrix[0];
        m_modelview[4] = glMatrix[4];
        m_modelview[8] = glMatrix[8];
        m_modelview[12] = glMatrix[12];
        // y
        m_modelview[1] = -glMatrix[1];
        m_modelview[5] = -glMatrix[5];
        m_modelview[9] = -glMatrix[9];
        m_modelview[13] = -glMatrix[13];
        // z
        m_modelview[2] = -glMatrix[2];
        m_modelview[6] = -glMatrix[6];
        m_modelview[10] = -glMatrix[10];
        m_modelview[14] = -glMatrix[14];

        // 0 0 0 1
        m_modelview[3] = 0;
        m_modelview[7] = 0;
        m_modelview[11] = 0;
        m_modelview[15] = 1;

        if (scale != undefined && scale !== 0.0) {
            m_modelview[12] *= scale;
            m_modelview[13] *= scale;
            m_modelview[14] *= scale;
        }

        glRhMatrix = m_modelview;

        return glRhMatrix;
    }

    transMatToGLMat(transMat, glMat, scale) {
        if (glMat == undefined) {
            glMat = new Float64Array(16);
        }

        glMat[0 + 0 * 4] = transMat[0]; // R1C1
        glMat[0 + 1 * 4] = transMat[1]; // R1C2
        glMat[0 + 2 * 4] = transMat[2];
        glMat[0 + 3 * 4] = transMat[3];
        glMat[1 + 0 * 4] = transMat[4]; // R2
        glMat[1 + 1 * 4] = transMat[5];
        glMat[1 + 2 * 4] = transMat[6];
        glMat[1 + 3 * 4] = transMat[7];
        glMat[2 + 0 * 4] = transMat[8]; // R3
        glMat[2 + 1 * 4] = transMat[9];
        glMat[2 + 2 * 4] = transMat[10];
        glMat[2 + 3 * 4] = transMat[11];
        glMat[3 + 0 * 4] = 0.0;
        glMat[3 + 1 * 4] = 0.0;
        glMat[3 + 2 * 4] = 0.0;
        glMat[3 + 3 * 4] = 1.0;

        if (scale != undefined && scale !== 0.0) {
            glMat[12] *= scale;
            glMat[13] *= scale;
            glMat[14] *= scale;
        }
        return glMat;
    }
    getMarker(markerIndex) {
        if (0 === this.getNFTMarker(this.id, markerIndex)) {
            return this.NFTMarkerInfo;
        }
    }
    _copyImageToHeap(sourceImage) {
        if (!sourceImage) {
            console.error("Error: no provided imageData to ARControllerNFT");
            return -99;
        }

        let data;

        if (sourceImage.data) {
            data = sourceImage.data;
        }


        if (this.videoLuma) {
            let q = 0;
            for (let p = 0; p < this.videoSize; p++) {
                let r = data[q + 0],
                    g = data[q + 1],
                    b = data[q + 2];
                this.videoLuma[p] = (r + r + r + b + g + g + g + g) >> 3;
                q += 4;
            }
        }

        if (this.dataHeap) {
            this.dataHeap.set(data);
            return this.detectMarker(this.id);
        }
        return -99;
    }
    async fetchRemoteData(url) {
        try {
            const response = await fetch(url, {
                responseType: "arraybuffer",
            })
            let buffer = await response.arrayBuffer()
            return new Uint8Array(buffer);
        } catch (error) {
            throw error;
        }
    }
    addEventListener(name, callback) {
        if (!this.listeners[name]) {
            this.listeners[name] = [];
        }
        this.listeners[name].push(callback);
    }
    removeEventListener(name, callback) {
        if (this.listeners[name]) {
            let index = this.listeners[name].indexOf(callback);
            if (index > -1) {
                this.listeners[name].splice(index, 1);
            }
        }
    }
}